/*!
* nsbase.enums v1.0
*
* Copyright 2012, nsbase LLC. All rights reserved.
*
* Author: zjk.
*
* Description:
*
* The library allows you to create client enum types in javascript which have the similar behaviors as c# enums. 
* It should be a rule that we always create those fixed possible values into a client enum, just like what we always do in server codes.
* It's a good way that creates a dynamic mechanism to convert the server enums into client enums automatically, to make sure they are always synchronized. 
* 
*/

(function () {
    nsbase.enums = new enumsFunc();
    nsbase.enums.rebuildEnumTypes();

    function enumsFunc() {
        var _self = this;
        var _dicUpperEnumTypes = {};

        //Enum type extension methods...
        _self.isEnumType = isEnumType;
        _self.isEnumItem = isEnumItem;
        _self.validate = validate;
        _self.buildEnumType = buildEnumType;
        _self.rebuildEnumTypes = rebuildEnumTypes;
        _self.getEnumTypeByKey = getEnumTypeByKey;

        function isEnumType(enumType) {
            /// <summary>
            /// Determins whether the 
            /// </summary>
            /// <param name="showErrors">view errors by custom error viewer after validating.</param>
            /// <param name="groupNames">[optional] If this argument is given, then only properties owned in these groups will be affected, 
            /// otherwise all properties will be affacted.</param>
            /// <returns></returns>
            return (enumType && enumType instanceof EnumType);
        }

        function isEnumItem(enumItem) {
            return (enumItem && enumItem instanceof EnumItem);
        }

        function validate(enumType) {
            if (_self.isEnumType(enumType) == false) {
                throw new Error("The enumType '" + JSON.stringify(enumType) + "' is not a valid enumType.");
            }
            return true;
        }

        function buildEnumType(enumTypeData, enumTypeKey) {
            return new EnumType(enumTypeData, enumTypeKey);
        }

        function rebuildEnumTypes() {
            var enumInstance = nsbase.enums
            for (var enumTypeKey in enumInstance) {
                var enumType = enumInstance[enumTypeKey];

                if (enumInstance.hasOwnProperty(enumTypeKey) && enumTypeKey.length > 0 && enumTypeKey[0] != "_" && typeof enumType == "object" && this.isEnumType(enumType) == false) {
                    enumInstance[enumTypeKey] = enumInstance.buildEnumType(enumType, enumTypeKey);
                }
            }
        }

        function getEnumTypeByKey(enumTypeKey) {
            if (enumTypeKey == null) {
                return null;
            }
            var enumType = _self[enumTypeKey];
            if (enumType == null) {
                if (_dicUpperEnumTypes._upperEnumTypes == null) {
                    _dicUpperEnumTypes._upperEnumTypes = {};
                    for (var itemKey in _self) {
                        var item = _self[itemKey];
                        if (_self.hasOwnProperty(itemKey) && this.isEnumType(item)) {
                            _dicUpperEnumTypes._upperEnumTypes[itemKey.toUpperCase()] = item;
                        }
                    }
                }
                enumType = _dicUpperEnumTypes._upperEnumTypes[enumTypeKey.toUpperCase()];
            }
            return enumType;
        }
    }
    enumsFunc.EnumType = EnumType;
    enumsFunc.EnumItem = EnumItem;

    //Enum Classes
    function EnumType(enumTypeData, enumTypeKey) {
        var _self = this;
        var _enumTypeData = enumTypeData;
        //caches
        var _itemsArray = null, _valuesArray = null, _textsArray = null, _dicItemsByValue = null, _dicItemsByUpperKey = null, _innerJsonData = null;

        function _init() {
            _self.addEnum = addEnum;
            _self.isEnum = isEnum;
            _self.validate = validate;
            _self.getEnums = getEnums;
            _self.getValues = getValues;
            _self.getTexts = getTexts;
            _self.getEnumByValue = getEnumByValue;
            _self.getTextByValue = getTextByValue;
            _self.getEnumByKey = getEnumByKey;
            _self.toJson = toJson;

            _self.key = "";
            if (enumTypeKey != null) {
                _self.key = enumTypeKey;
            }

            buildEnumItems(enumTypeKey);
        }

        function addEnum(enumItemData, enumItemKey) {
            /// <signature>
            ///   <summary>Add a new enum item.</summary>
            ///   <param name="enumItemData" type="Object">The data of new enum item.</param>
            ///   <param name="enumItemKey" type="String">The key of new enum item.</param>
            /// </signature>
            validateEnumItem(enumItemKey, enumItemData.value);

            var newEnumItem = new EnumItem(enumItemData, enumItemKey)
            _self[enumItemKey] = newEnumItem;

            //clear caches
            _itemsArray = null;
            _valuesArray = null;
            _textsArray = null;
            _dicItemsByValue = null;
            _dicItemsByUpperKey = null;
            _innerJsonData = null;

            return newEnumItem;
        }

        function isEnum(enumItem) {
            /// <signature>
            ///   <summary>Check if an object is a valid enum item.</summary>
            ///   <param name="enumItem" type="Object">The object to check.</param>
            ///   <returns type="Boolean" />
            /// </signature>
            return (enumItem instanceof EnumItem && enumItem == this[enumItem.key]);
        }

        function validate(enumItem) {
            /// <signature>
            ///   <summary>Validate if an object is a valid enum item. If the validation failed, then an error will be thrown.</summary>
            ///   <param name="enumItem" type="Object">The object to validate.</param>
            /// </signature>
            var me = this;
            if (!me.isEnum(enumItem)) {
                throw new Error("The object '" + JSON.stringify(enumItem) + "' is not a valid enum item of '" + _self.key + "'!");
            }
            return true;
        }

        function getEnums(excludeItems) {
            /// <signature>
            ///   <summary>Get all enum items array exclude some specific enum items.</summary>
            ///   <param name="excludeItems" type="Array" elementType="EnumItem">The specific enum items to exlcude.</param>
            ///   <returns type="Array" elementType="EnumItem" />
            /// </signature>
            /// <signature>
            ///   <summary>Get all enum items array.</summary>
            ///   <returns type="Array" elementType="EnumItem" />
            /// </signature>
            var me = this;
            if (_itemsArray == null) {
                _itemsArray = [];
                for (var itemKey in me) {
                    var item = me[itemKey];
                    if (me.hasOwnProperty(itemKey) && itemKey.length > 0 && itemKey[0] != "_" && typeof item == "object" && item.value !== undefined) {
                        _itemsArray.push(item);
                    }
                }
                _itemsArray.sort(function (a, b) {
                    return a.order > b.order;
                });
            }
            var allItems = _itemsArray;
            //filter those excluded items.
            if (excludeItems != null) {
                if (arguments.length > 1) {
                    excludeItems = Array.prototype.slice.call(arguments);
                }
                var newArray = [];
                for (var i = 0; i < allItems.length; i++) {
                    var item = allItems[i];
                    for (var j = 0; j < excludeItems.length; j++) {
                        var excludeItem = excludeItems[j];
                        if (item != excludeItem) {
                            newArray.push(item);
                        }
                    }
                }
                return newArray;
            }
            return allItems;
        };

        function getValues() {
            /// <signature>
            ///   <summary>Get values array of all enum items.</summary>
            ///   <returns type="Array" elementType="String" />
            /// </signature>
            var me = this;
            if (_valuesArray == null) {
                var values = [];
                var itemsArray = me.getEnums();
                for (var i = 0; i < itemsArray.length; i++) {
                    values.push(itemsArray[i].value);
                }
                _valuesArray = values;
            }
            return _valuesArray;
        };

        function getTexts() {
            /// <signature>
            ///   <summary>Get texts array of all enum items.</summary>
            ///   <returns type="Array" elementType="String" />
            /// </signature>
            var me = this;
            if (_textsArray == null) {
                var texts = [];
                var itemsArray = me.getEnums();
                for (var i = 0; i < itemsArray.length; i++) {
                    texts.push(itemsArray[i].text);
                }
                _textsArray = texts;
            }
            return _textsArray;
        };

        function getEnumByValue(enumValue) {
            /// <signature>
            ///   <summary>Get an enum item by enum value.</summary>
            ///   <param name="enumValue" type="Number">The enum value used to find an enum item.</param>
            ///   <returns type="EnumItem" />
            /// </signature>
            var me = this;
            if (_dicItemsByValue == null) {
                _dicItemsByValue = {};
                var itemsArray = me.getEnums();
                for (var i = 0; i < itemsArray.length; i++) {
                    var item = itemsArray[i];
                    _dicItemsByValue[item.value] = item;
                }
            }
            return _dicItemsByValue[enumValue];
        };

        function getTextByValue(enumValue) {
            /// <signature>
            ///   <summary>Get the enum text by enum value.</summary>
            ///   <param name="enumValue" type="Number">The enum value used to find enum text.</param>
            ///   <returns type="String" />
            /// </signature>
            var me = this;
            var item = me.getEnumByValue(enumValue);
            if (item) {
                return item.text;
            }
            return "";
        };

        function getEnumByKey(enumKey) {
            /// <signature>
            ///   <summary>Get an enum item by enum key.</summary>
            ///   <param name="enumKey" type="String">The enum key used to find an enum item.</param>
            ///   <returns type="EnumItem" />
            /// </signature>
            if (enumKey == null) {
                return null;
            }
            var me = this;
            var enumItem = me[enumKey];
            if (enumItem == null) {
                if (_dicItemsByUpperKey == null) {
                    _dicItemsByUpperKey = {};
                    var itemsArray = me.getEnums();
                    for (var i = 0; i < itemsArray.length; i++) {
                        var item = itemsArray[i];
                        _dicItemsByUpperKey[item.key.toUpperCase()] = item;
                    }
                }
                enumItem = _dicItemsByUpperKey[enumKey.toUpperCase()];
            }
            return enumItem;
        };

        function toJson() {
            /// <signature>
            ///   <summary>Convert all enum items to json format. The data structure is: {{value1}: {text1}, {value2}: {text2}}</summary>
            ///   <returns type="Object" />
            /// </signature>
            var me = this;
            if (_innerJsonData == null) {
                _innerJsonData = {};
                var itemArray = me.getEnums();
                for (var i = 0; i < itemArray.length; i++) {
                    var item = itemArray[i];
                    _innerJsonData[item.value] = item.text;
                }
            }
            return _innerJsonData;
        };

        function buildEnumItems(enumTypeKey) {
            var valueDic = {};
            var types = [], typeDic = {};
            for (var enumItemKey in _enumTypeData) {
                if (_enumTypeData.hasOwnProperty(enumItemKey) && enumItemKey.length > 0 && enumItemKey[0] != "_" && typeof _enumTypeData[enumItemKey] == "object" && _enumTypeData[enumItemKey].value !== undefined) {
                    var enumItem = _enumTypeData[enumItemKey];
                    validateEnumItem(enumItemKey, enumItem.value);
                    _self[enumItemKey] = new EnumItem(enumItem, enumItemKey);
                    //validate only one value type
                    if (enumItem.value != "") {
                        var valueType = typeof enumItem.value;
                        if (!typeDic.hasOwnProperty(valueType)) {
                            typeDic[valueType] = valueType;
                            types.push(valueType);
                        }
                        if (types.length > 1) {
                            throw new Error("The enumType '" + enumTypeKey + "' has various value types '" + types.join(", ") + "'.");
                        }
                    }
                    //validate duplicate values.
                    if (valueDic.hasOwnProperty(enumItem.value)) {
                        throw new Error("The enumType '" + enumTypeKey + "' has duplicate value '" + enumItem.value + "'.");
                    }
                    valueDic[enumItem.value] = enumItem.value;
                }
            }
        };

        function validateEnumItem(key, value) {
            if (key == null) {
                throw new Error("Enum item validates failed! The 'key' can't be null.");
            }
            if (value != "") {
                if (value == null) {
                    throw new Error("Enum item validates failed! The 'value' can't be null.");
                }
                var valueString = value.toString();
                if (typeof valueString != "string" || (valueString.match(/^\-?\d+$/) == null && $.inArray(valueString.toLocaleLowerCase(), ["true", "false"]) == -1)) {
                    throw new Error("New enum added failed! The value of enum item must be a number, but the actual value is '" + JSON.stringify(value) + "'.");
                }
            }
        };

        _init();
    }

    function EnumItem(enumItemData, enumItemKey) {
        /// <signature>
        ///   <summary>
        ///     Represents an enum item.
        ///   </summary>
        /// </signature>
        /// <field name="key" type="String">The mandatory key of enum item.</field>
        /// <field name="value" type="Number">The mandatory value of enum item. Most of enum values are a Number, and other available value types are: Boolean and the empty string.</field>
        /// <field name="text" type="String">The text of enum item. If no text is given, then enum key is used.</field>
        /// <field name="order" type="Number">The order of enum item. It's used to sort the enum items for displaying. If no value is given, then the original physical order is used.</field>

        var _self = this;

        function _init() {
            _self.key = (enumItemKey != null ? enumItemKey : enumItemData.key);
            _self.value = enumItemData.value;
            _self.text = enumItemData.text;
            _self.order = enumItemData.order;

            _self.toString = toString;

            formatEnumValue();

            if (_self.text === undefined) {
                _self.text = enumItemKey;
            }
            if (_self.order === undefined || _self.order === 0) {
                _self.order = _self.value;
            }
        }

        function formatEnumValue() {
            var value = _self.value;
            if (value == null) {
                value = "";
            }
            if (value != "") {
                if (typeof value == "string" && $.inArray(value.toUpperCase(), ["TRUE", "FALSE"]) != -1) {
                    value = (value.toUpperCase() == "TRUE");
                }
                else {
                    var intValue = parseInt(value);
                    if (isNaN(intValue)) {
                        throw new Error("The value '" + JSON.stringify(value) + "' of enumItem is invalid. It can only be a number or empty string.");
                    }
                    value = intValue;
                }
            }
            _self.value = value;
        }


        function toString() {
            return this.value;
        }

        _init();
    }
})();